#!/bin/bash
# BOTSOL macOS launcher — template. build-app.sh fills in BOTSOL Bot,
# BOTSOL Bot and index.js.
#
# Design rules from TECH_SPEC_V5 §2 and §7:
#   - The app NEVER writes inside its own bundle. All writable state (.env, the
#     SQLite db) lives in a per-user Application Support folder.
#   - A visible Terminal window, so a CLI trading bot is never a silent
#     invisible process.
#   - Node is detected across common install locations and version-checked; if
#     none is usable, the user is guided to install it rather than met with a
#     cryptic failure.

APP_NAME="BOTSOL Bot"
WORK_DIR="$HOME/Library/Application Support/BOTSOL Bot"
ENTRY="index.js"

# Resolve the bundle's own Resources/app directory (read-only).
HERE="$(cd "$(dirname "$0")" && pwd)"
APP_SRC="$(cd "$HERE/../Resources/app" && pwd)"

# ── Re-launch inside Terminal if double-clicked from Finder ──────────────────
# Finder runs this with no attached terminal. Detect that and relaunch visibly.
# BOTSOL_NO_TERMINAL=1 skips this — useful when already in a terminal, over SSH,
# or in CI, where opening Terminal.app is wrong or impossible.
if [ "${BOTSOL_NO_TERMINAL:-}" != "1" ] && [ ! -t 1 ]; then
  osascript >/dev/null 2>&1 <<OSA
tell application "Terminal"
  activate
  do script "'$HERE/$(basename "$0")'"
end tell
OSA
  exit 0
fi

echo "──────────────────────────────────────────────"
echo "  $APP_NAME"
echo "──────────────────────────────────────────────"
echo

# ── Node detection ───────────────────────────────────────────────────────────
# Every candidate is checked for the CAPABILITY the app needs, not merely for
# existence — a Mac can easily have an old node first on PATH (a stale
# /usr/local/bin, or the system /usr/bin/node) that would shadow a good one.
#
# The floor is a capability rather than a major version on purpose.
# @solana/web3.js reaches an ESM-only `uuid` through rpc-websockets via
# require(), so the app needs Node's require(esm) support. That landed in 20.19
# and in 22.12 — but NOT in 22.0–22.11 — so no single "major >= N" test is
# correct: it would either admit a broken 22.11 or reject a working 20.19.
#
# Measured by loading the app's real require chain (index.js, lib/setupWizard.js
# and the lazy poolRefresh/onchainArb requires) under each runtime:
#
#   v18.20.4  require_module undefined  chain FAILS (ERR_REQUIRE_ESM)
#   v20.18.0  require_module undefined  chain FAILS
#   v20.19.0  require_module true       chain loads
#   v22.11.0  require_module false      chain FAILS
#   v22.12.0  require_module true       chain loads
#   v24.18.0  require_module true       chain loads
#
# process.features.require_module === true matches "the chain loads" exactly, so
# that is the gate. It also stays correct without edits if a future dependency
# shifts the boundary again.
NODE_MIN_DISPLAY="20.19 or newer (22.12+ in the 22 line)"

# The version string, e.g. "v20.19.0". Empty when the binary will not run.
node_version() {
  "$1" --version 2>/dev/null | head -n1
}

# The real gate: can this Node require() an ES module?
node_is_supported() {
  [ "$("$1" -p 'String(process.features.require_module)' 2>/dev/null | tail -n1)" = "true" ]
}

# The install locations to probe, in the order we prefer them.
node_candidates() {
  printf '%s\n' /usr/local/bin/node /opt/homebrew/bin/node

  # nvm installs, newest first. Sort each version field numerically — a plain
  # glob is lexical, which would rank v9 above v18.
  local nvm_dir="$HOME/.nvm/versions/node" version
  if [ -d "$nvm_dir" ]; then
    ls -1 "$nvm_dir" 2>/dev/null | sed -n 's/^v//p' \
      | sort -t. -k1,1nr -k2,2nr -k3,3nr \
      | while IFS= read -r version; do
          printf '%s\n' "$nvm_dir/v$version/bin/node"
        done
  fi

  printf '%s\n' /usr/bin/node
}

# Sets NODE to the first usable interpreter. Assigns globals rather than
# printing, so the "found one, but it is too old" detail survives for the error
# message — a command substitution would run this in a subshell and lose it.
find_node() {
  NODE=""
  NODE_TOO_OLD=""
  local candidate version

  # Remember the first candidate that RUNS but cannot require(esm), so the
  # failure message can name it instead of claiming nothing is installed.
  remember_too_old() {
    [ -n "$NODE_TOO_OLD" ] && return 0
    version="$(node_version "$1")"
    [ -n "$version" ] && NODE_TOO_OLD="$1 ($version)"
  }

  # PATH first — that is the node the user's own shell would pick.
  candidate="$(command -v node 2>/dev/null)"
  if [ -n "$candidate" ]; then
    if node_is_supported "$candidate"; then NODE="$candidate"; return 0; fi
    remember_too_old "$candidate"
  fi

  while IFS= read -r candidate; do
    [ -x "$candidate" ] || continue
    if node_is_supported "$candidate"; then NODE="$candidate"; return 0; fi
    remember_too_old "$candidate"
  done < <(node_candidates)

  return 1
}

find_node
if [ -z "$NODE" ]; then
  if [ -n "$NODE_TOO_OLD" ]; then
    echo "The Node.js on this Mac is too old to run $APP_NAME."
    echo
    echo "  Found: $NODE_TOO_OLD"
  else
    echo "Node.js was not found on this Mac."
  fi
  echo
  echo "  $APP_NAME needs Node.js $NODE_MIN_DISPLAY."
  echo "  Install it from https://nodejs.org (the LTS build), then reopen this app."
  echo
  echo "  If Node.js is already installed, move $APP_NAME into your"
  echo "  /Applications folder and open it from there, then try again."
  echo
  echo "Press any key to close."
  read -r -n 1
  exit 1
fi
echo "Using Node: $NODE ($("$NODE" --version))"

# ── Writable per-user working folder ─────────────────────────────────────────
mkdir -p "$WORK_DIR"

# cwd is the WRITABLE folder, so .env, the SQLite db, and anything the app
# writes land outside the read-only bundle.
cd "$WORK_DIR" || exit 1
export DB_PATH="$WORK_DIR/botsol.db"

# ── First-run setup ──────────────────────────────────────────────────────────
# If the app ships a setup wizard (the bot does; the licence server does not),
# run it as its own process first. It seeds .env from the bundled example,
# collects the connection values via a local 127.0.0.1 web form, and skips
# instantly once already configured — completing BEFORE the app starts so
# config is never read before it is written.
WIZARD="$APP_SRC/scripts/runSetupWizard.js"
if [ -f "$WIZARD" ]; then
  "$NODE" "$WIZARD" || exit 1
elif [ ! -f "$WORK_DIR/.env" ]; then
  # No wizard: seed .env and open it for manual editing (licence server).
  cp "$APP_SRC/.env.example" "$WORK_DIR/.env" 2>/dev/null || touch "$WORK_DIR/.env"
  # A seeded .env holds secrets — the bot's wallet private key, or the licence
  # server's ADMIN_SECRET and payout addresses. `cp` copies the example's
  # world-readable mode, and every local macOS account is in group `staff`, so
  # tighten it. Pure hardening, no behaviour change — applies to both apps.
  chmod 600 "$WORK_DIR/.env" 2>/dev/null || true
  echo
  echo "First run — created your configuration file at:"
  echo "  $WORK_DIR/.env"
  echo
  echo "Opening it now. Fill it in, save, then reopen this app."
  open -e "$WORK_DIR/.env"
  echo "Press any key to close once you have saved your settings."
  read -r -n 1
  exit 0
fi

# ── Trading mode: dry run <-> live (bot only) ────────────────────────────────
# A deliberate, typed switch so a non-technical customer never hand-edits a
# hidden .env to move real money. The friction (typing LIVE) is intentional.
# Gated to the BOT app only: entry is index.js AND the setup wizard exists, so a
# future app that merely reuses index.js does not silently inherit a trading
# prompt. Never the licence server. Runs after config is settled, before exec.
if [ "$ENTRY" = "index.js" ] && [ -f "$WIZARD" ]; then
  ENV_FILE="$WORK_DIR/.env"

  # Current mode. A missing/blank/unrecognised DRY_RUN counts as dry run (true),
  # matching the bot's own default; only an explicit false value is "live".
  dry_run_value() {
    local v
    v="$(grep -E '^[[:space:]]*DRY_RUN=' "$ENV_FILE" 2>/dev/null | tail -n1 \
         | sed -E 's/^[[:space:]]*DRY_RUN=[[:space:]]*//; s/[[:space:]]*$//')"
    case "$(printf '%s' "$v" | tr '[:upper:]' '[:lower:]')" in
      false|0|no|off) echo "false" ;;
      *) echo "true" ;;
    esac
  }

  # Rewrite DRY_RUN (or ADD it if absent), touching no other line, then restore
  # 600 — sed -i '' renames a temp over the file and can drop permissions.
  set_dry_run() {
    if grep -qE '^[[:space:]]*DRY_RUN=' "$ENV_FILE" 2>/dev/null; then
      sed -i '' -E "s|^[[:space:]]*DRY_RUN=.*|DRY_RUN=$1|" "$ENV_FILE"
    else
      printf 'DRY_RUN=%s\n' "$1" >> "$ENV_FILE"
    fi
    chmod 600 "$ENV_FILE" 2>/dev/null || true
  }

  # The size the user is about to trade — shown BEFORE they decide, and again
  # after switching to live.
  size_line() {
    local ms
    ms="$(grep -E '^[[:space:]]*MAX_TRADE_SIZE_SOL=' "$ENV_FILE" 2>/dev/null | tail -n1 \
          | sed -E 's/^[[:space:]]*MAX_TRADE_SIZE_SOL=[[:space:]]*//; s/[[:space:]]*$//')"
    if [ -n "$ms" ]; then
      echo "  Trade size per attempt: MAX_TRADE_SIZE_SOL = $ms SOL"
    else
      echo "  Trade size per attempt: MAX_TRADE_SIZE_SOL is unset (bot default 0.1 SOL)"
    fi
  }

  CURRENT_MODE="$(dry_run_value)"

  if [ "${BOTSOL_NO_TERMINAL:-}" = "1" ] || [ ! -t 0 ]; then
    # Headless / SSH / CI / piped stdin: never prompt, never hang — use .env as-is.
    if [ "$CURRENT_MODE" = "false" ]; then
      echo "Mode: LIVE (DRY_RUN=false in .env; prompt skipped — no interactive terminal)."
    else
      echo "Mode: DRY RUN (prompt skipped — no interactive terminal)."
    fi
  elif [ "$CURRENT_MODE" = "true" ]; then
    echo "────────────────────────────────────────────"
    echo "  Mode: DRY RUN — no real money will move."
    echo "────────────────────────────────────────────"
    size_line
    echo
    echo "  Press Enter to continue in dry run."
    echo "  Type LIVE and press Enter to trade real money."
    echo
    printf "  > "
    read -r MODE_REPLY
    if [ "$MODE_REPLY" = "LIVE" ]; then
      set_dry_run false
      echo
      echo "  LIVE mode set — real transactions WILL be submitted."
      size_line
      echo
    else
      echo
      echo "  Staying in dry run. Nothing will be submitted."
      echo
    fi
  else
    echo "────────────────────────────────────────────"
    echo "  Mode: LIVE — real transactions WILL be submitted."
    echo "────────────────────────────────────────────"
    size_line
    echo
    echo "  Press Enter to continue live."
    echo "  Type DRY and press Enter to go back to dry run."
    echo
    printf "  > "
    read -r MODE_REPLY
    if [ "$MODE_REPLY" = "DRY" ]; then
      set_dry_run true
      echo
      echo "  Switched back to DRY RUN. Nothing will be submitted."
      echo
    else
      echo
      echo "  Continuing LIVE."
      echo
    fi
  fi
fi

# ── Run ──────────────────────────────────────────────────────────────────────

echo
echo "Starting $APP_NAME. Leave this window open while it runs."
echo "Press Control-C to stop."
echo "──────────────────────────────────────────────"
echo
exec "$NODE" "$APP_SRC/$ENTRY"
